﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Web.Mvc;
using System.Reflection;
using System.ComponentModel;
using System.Data.Linq.Mapping;

using Lilium.ComponentModel;
using Lilium.Reflection;
using System.Data.Linq;

namespace Lilium.Web.Mvc.Metadata
{
	//TODO: навести порядо тут, убрать все расширения, кроме EntityMetadata, добавить EntityMetadataProvider в конструктор
	public class DataAnnotationsModelMetadataExtender : DataAnnotationsModelMetadataProvider
	{
		public DataAnnotationsModelMetadataExtender(IEntityMetadataProvider entityMetadataProvider)
		{
			if (entityMetadataProvider == null) throw new ArgumentNullException("entityMetadataProvider");

			m_EntityMetadataProvider = entityMetadataProvider;
		}
		/*
		public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType)
		{
			foreach (var metadata in BaseMetadataProvider.GetMetadataForProperties(container, containerType))
			{
				ExtendMetadata(metadata);
				yield return metadata;
			}
		}

		public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
		{
			var metadata = BaseMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
			ExtendMetadata(metadata);
			return metadata;
		}

		public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType)
		{
			var metadata = BaseMetadataProvider.GetMetadataForType(modelAccessor, modelType);
			ExtendMetadata(metadata);
			return metadata;
		}
		*/
		protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
		{
			// большинство расширений можно загрузить из атрибутов. в принципе.
			var baseMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
			
			//ExtendMetadata(baseMetadata);
			EntityMetadataProvider.ExtendMetadata(baseMetadata, attributes);
			return baseMetadata;
		}

		[Obsolete("Это все нахуй")]
		private void ExtendMetadata(ModelMetadata modelMetadata)
		{
			if (modelMetadata == null) throw new ArgumentNullException("modelMetadata");

			if (!string.IsNullOrEmpty(modelMetadata.PropertyName))
			{
				var property = modelMetadata.ContainerType.GetProperty(modelMetadata.PropertyName);
				
				var propertyDescriptor = GetPropertyDescriptor(modelMetadata.ContainerType, modelMetadata.PropertyName);

				AddAssociationMetadata(property, modelMetadata);
				AddForeignKeyMetadata(modelMetadata, modelMetadata);
			}
		}

		private static void AddAssociationMetadata(PropertyInfo propertyInfo, ModelMetadata metadata)
		{
			if (propertyInfo == null) throw new ArgumentNullException("propertyInfo");
			if (metadata == null) throw new ArgumentNullException("metadata");

			var associationAttribute = propertyInfo.FindCustomAttribute<System.Data.Linq.Mapping.AssociationAttribute>();
			if (associationAttribute != null)
				metadata.AddExtension(new AssociationMetadataExtension(
					GetReferencedType(propertyInfo.PropertyType),
					associationAttribute.IsForeignKey,
					GetThisKey(associationAttribute, propertyInfo.ReflectedType),
					GetOtherKey(associationAttribute, propertyInfo.PropertyType),
					GetDisplayNameColumn(propertyInfo.PropertyType),
					GetSortColumn(propertyInfo.PropertyType)), null);
		}
		
		private static void AddForeignKeyMetadata(ModelMetadata modelMetadata, ModelMetadata metadata)
		{
			if (modelMetadata == null) throw new ArgumentNullException("modelMetadata");
			if (metadata == null) throw new ArgumentNullException("metadata");

			var foreignKeyMetadata =
				(from referencePropertyInfo in modelMetadata.ContainerType.GetProperties()
				 let associationAttribute = referencePropertyInfo.FindCustomAttribute<System.Data.Linq.Mapping.AssociationAttribute>()
				 where associationAttribute != null && associationAttribute.ThisKey == modelMetadata.PropertyName && associationAttribute.IsForeignKey
				 select new AssociationMetadataExtension(
					 GetReferencedType(referencePropertyInfo.PropertyType),
					 associationAttribute.IsForeignKey,
					 GetThisKey(associationAttribute, referencePropertyInfo.ReflectedType),
					 GetOtherKey(associationAttribute, referencePropertyInfo.PropertyType),
					 GetDisplayNameColumn(referencePropertyInfo.PropertyType),
					 GetSortColumn(referencePropertyInfo.PropertyType))).FirstOrDefault();
			if (foreignKeyMetadata != null)
				metadata.AddExtension(foreignKeyMetadata, null);
		}

		private static string GetDisplayNameColumn(Type entityType)
		{
			if (entityType == null) throw new ArgumentNullException("entityType");

			return entityType.GetCustomAttributes(typeof(DisplayColumnAttribute), true).Cast<DisplayColumnAttribute>().Select(a => a.DisplayColumn).FirstOrDefault();
		}

		private static string GetSortColumn(Type entityType)
		{
			if (entityType == null) throw new ArgumentNullException("entityType");

			return entityType.GetCustomAttributes(typeof(DisplayColumnAttribute), true).Cast<DisplayColumnAttribute>().Select(a => !string.IsNullOrEmpty(a.SortColumn) ? a.SortColumn : a.DisplayColumn).FirstOrDefault();
		}

		private static string GetOtherKey(System.Data.Linq.Mapping.AssociationAttribute associationAttribute, Type entityType)
		{
			if (associationAttribute == null) throw new ArgumentNullException("associationAttribute");

			return
				!string.IsNullOrEmpty(associationAttribute.OtherKey)
				? associationAttribute.OtherKey
				: GetPrimaryKeyPropertyName(entityType);
		}

		private static string GetPrimaryKeyPropertyName(Type entityType)
		{
			if (entityType == null) throw new ArgumentNullException("entityType");

			return (from otherProperty in entityType.GetProperties()
					let columnAttribute = otherProperty.GetCustomAttributes(typeof(ColumnAttribute), true).Cast<ColumnAttribute>().FirstOrDefault()
					where columnAttribute != null && columnAttribute.IsPrimaryKey
					select otherProperty.Name).FirstOrDefault();
		}

		private static PropertyDescriptor GetPropertyDescriptor(Type entityType, string propertyName)
		{
			if (entityType == null) throw new ArgumentNullException("entityType");

			var typeDescriptionProvider = new AssociatedMetadataTypeTypeDescriptionProvider(entityType);
			var typeDescriptor = typeDescriptionProvider.GetTypeDescriptor(entityType);
			return typeDescriptor.GetProperties()[propertyName];
		}

		private static string GetThisKey(System.Data.Linq.Mapping.AssociationAttribute associationAttribute, Type entityType)
		{
			if (associationAttribute == null) throw new ArgumentNullException("associationAttribute");

			return
				!string.IsNullOrEmpty(associationAttribute.ThisKey)
				? associationAttribute.ThisKey
				: GetPrimaryKeyPropertyName(entityType);
		}

		private static Type GetReferencedType(Type rawType)
		{
			if (rawType == null) throw new ArgumentNullException("rawType");

			if (rawType.IsGenericType && typeof(EntitySet<>).IsAssignableFrom(rawType.GetGenericTypeDefinition()))
			{
				return rawType.GetGenericArguments()[0];
			}
			return rawType;
		}

		#region private IEntityMetadataProvider EntityMetadataProvider

		private readonly IEntityMetadataProvider m_EntityMetadataProvider;

		private IEntityMetadataProvider EntityMetadataProvider
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_EntityMetadataProvider;
			}
		}

		#endregion
	}
}